﻿using System;

namespace OpenLightGroup.Common
{

    /// <summary>
    /// Calculates and contains the result of age calculations.
    /// </summary>
    /// <remarks>
    /// Adapted from information posted here: http://stackoverflow.com/questions/9/how-do-i-calculate-someones-age-in-c
    /// </remarks>
    public struct Age :
    IEquatable<Age>, IComparable<Age>
    {

        #region Fields

        private readonly int _years;
        private readonly int _months;
        private readonly int _days;

        #endregion

        #region Ctor

        /// <summary>
        /// Private constructor used by the factory methods.
        /// </summary>
        /// <param name="years"></param>
        /// <param name="months"></param>
        /// <param name="days"></param>
        private Age(int years, int months, int days)
            : this()
        {
            this._years = years;
            this._months = months;
            this._days = days;
        }

        /// <summary>
        /// Calls the FromDate(startDate) method and constructs a new instance.
        /// </summary>
        /// <param name="startDate"></param>
        public Age(DateTime startDate)
            : this(startDate, DateTime.Now)
        {
        }

        /// <summary>
        /// Calls the FromDates(startDate, endDate) method and constructs a new instance.
        /// </summary>
        public Age(DateTime startDate, DateTime endDate)
            : this()
        {
            var age = Age.FromDates(startDate, endDate);
            this._days = age.Days;
            this._months = age.Months;
            this._years = age.Years;

        }

        /// <summary>
        /// Calls the FromTimeSpan(offset) method and constructs a new instance.
        /// </summary>
        public Age(TimeSpan offset)
            : this(offset, DateTime.Now)
        {
        }

        /// <summary>
        /// Calls the FromTimeSpan(offset, endDate) method and constructs a new instance.
        /// </summary>
        public Age(TimeSpan offset, DateTime endDate)
            : this()
        {
            var age = Age.FromTimeSpan(offset, endDate);
            this._days = age.Days;
            this._months = age.Months;
            this._years = age.Years;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the years.
        /// </summary>
        public int Years
        {
            get
            {
                return this._years;
            }
        }

        /// <summary>
        /// Gets the months.
        /// </summary>
        public int Months
        {
            get
            {
                return this._months;
            }
        }

        /// <summary>
        /// Gets the days.
        /// </summary>
        public int Days
        {
            get
            {
                return this._days;
            }
        }

        #endregion

        #region Static Facotries

        /// <summary>
        /// Calculates age from startDate to now.
        /// </summary>
        /// <param name="startDate">The start date. This could be the date of birth for example.</param>
        /// <returns></returns>
        public static Age FromDate(DateTime startDate)
        {
            DateTime endDate = DateTime.Now;

            return FromDates(startDate, endDate);

        }

        /// <summary>
        /// Calculates age from startDate to endDate.
        /// </summary>
        /// <param name="startDate">The start date. This could be the date of birth for example.</param>
        /// <param name="endDate">The end date. The later date to use in the calculation of age.</param>
        /// <returns></returns>
        public static Age FromDates(DateTime startDate, DateTime endDate)
        {

            int days = endDate.Day - startDate.Day;
            if (days < 0)
            {
                endDate = endDate.AddMonths(-1);
                days += DateTime.DaysInMonth(endDate.Year, endDate.Month);
            }

            int months = endDate.Month - startDate.Month;
            if (months < 0)
            {
                endDate = endDate.AddYears(-1);
                months += 12;
            }

            int years = endDate.Year - startDate.Year;

            Age age = new Age(years, months, days);

            return age;

        }

        /// <summary>
        /// Calculates age by using now minus the offset as the start date and now as the end date.
        /// </summary>
        /// <param name="offset">The offset. Subtracted from now to get start date.</param>
        /// <returns></returns>
        public static Age FromTimeSpan(TimeSpan offset)
        {
            DateTime endDate = DateTime.Now;

            return FromTimeSpan(offset, endDate);
        }

        /// <summary>
        /// Calculates age by using <paramref name="endDate"/> minus the offset as the start date and  the <paramref name="endDate"/>.
        /// </summary>
        /// <param name="offset">The offset. Subtracted from <paramref name="endDate"/> to get start date.</param>
        /// <param name="endDate"></param>       
        /// <returns></returns>
        public static Age FromTimeSpan(TimeSpan offset, DateTime endDate)
        {
            return FromDates(endDate - offset, endDate);
        }

        #endregion

        #region Object Overrides

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode()
        {
            return (this.Years.ToString("D9") + this.Months.ToString("D2") + this.Days.ToString("D2")).GetHashCode();
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        ///   <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public override bool Equals(object obj)
        {
            Age age = new Age();

            try
            {
                age = (Age)obj;
            }
            catch
            {
                return false;
            }

            return this.Equals(age);
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {

            return String.Format("{0} year{1}, {2} month{3} and {4} day{5}",
                                 this.Years, (this.Years == 1) ? "" : "s",
                                 this.Months, (this.Months == 1) ? "" : "s",
                                 this.Days, (this.Days == 1) ? "" : "s");
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <param name="formatString">The format string.</param>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public string ToString(string formatString)
        {
            return String.Format(formatString, this.Years, this.Months, this.Days);
        }

        #endregion

        #region IEquatable<Age> Members

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
        /// </returns>
        public bool Equals(Age other)
        {
            return this.Years == other.Years
                   && this.Months == other.Months
                   && this.Days == other.Days;
        }

        #endregion

        #region IComparable<Age> Members

        /// <summary>
        /// Compares the current object with another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings:
        /// Value
        /// Meaning
        /// Less than zero
        /// This object is less than the <paramref name="other"/> parameter.
        /// Zero
        /// This object is equal to <paramref name="other"/>.
        /// Greater than zero
        /// This object is greater than <paramref name="other"/>.
        /// </returns>
        public int CompareTo(Age other)
        {

            var output = this.Years.CompareTo(other.Years);

            if (output == 0)
            {

                output = this.Months.CompareTo(other.Months);

                if (output == 0)
                {
                    output = this.Days.CompareTo(other.Days);

                }

            }

            return output;

        }

        #endregion

    }

}